-module(bmula_webtool).
 -include("bmula.inc").

-export([initialize/1, index/2, left/2, right/2]).
-export([dl/1, dl/2, ul/1, ul/2]).

-type webtool_linkdata() :: {'web_data', {string(), string()}}.
-type webtool_alias() :: {'alias', {string(), string()}} | {'alias', {'erl_alias', string(), [atom()]}}.
-type webtool_start() :: {'start', {'app', atom()} | {'child', term()} | {'func', mfa(), mfa()}}.
-type webtool_callback_result() :: {atom(), [webtool_linkdata() | webtool_alias() | webtool_start()]} | 'error'.

%% {{{ taken from mochiweb
-define(PERCENT, 37).  % $\%
-define(FULLSTOP, 46). % $\.
-define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse
                    (C >= $a andalso C =< $f) orelse
                    (C >= $A andalso C =< $F))).
-define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse
                     (C >= $A andalso C =< $Z) orelse
                     (C >= $0 andalso C =< $9) orelse
                     (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse
                      C =:= $_))).

hexdigit(C) when C < 10 -> $0 + C;
hexdigit(C) when C < 16 -> $A + (C - 10).

unhexdigit(C) when C >= $0, C =< $9 -> C - $0;
unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10;
unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10.

revjoin([], _Separator, Acc) ->
    Acc;
revjoin([S | Rest], Separator, []) ->
    revjoin(Rest, Separator, [S]);
revjoin([S | Rest], Separator, Acc) ->
    revjoin(Rest, Separator, [S, Separator | Acc]).

%% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string()
%% @doc URL safe encoding of the given term.
quote_plus(Atom) when is_atom(Atom) ->
    quote_plus(atom_to_list(Atom));
quote_plus(Int) when is_integer(Int) ->
    quote_plus(integer_to_list(Int));
quote_plus(Binary) when is_binary(Binary) ->
    quote_plus(binary_to_list(Binary));
quote_plus(String) ->
    quote_plus(String, []).

quote_plus([], Acc) ->
    lists:reverse(Acc);
quote_plus([C | Rest], Acc) when ?QS_SAFE(C) ->
    quote_plus(Rest, [C | Acc]);
quote_plus([$\s | Rest], Acc) ->
    quote_plus(Rest, [$+ | Acc]);
quote_plus([C | Rest], Acc) ->
    <<Hi:4, Lo:4>> = <<C>>,
    quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]).

%% @spec urlencode([{Key, Value}]) -> string()
%% @doc URL encode the property list.
urlencode(Props) ->
    RevPairs = lists:foldl(fun ({K, V}, Acc) ->
                                   [[quote_plus(K), $=, quote_plus(V)] | Acc]
                           end, [], Props),
    lists:flatten(revjoin(RevPairs, $&, [])).

%% @spec parse_qs(string() | binary()) -> [{Key, Value}]
%% @doc Parse a query string or application/x-www-form-urlencoded.
parse_qs(Binary) when is_binary(Binary) ->
    parse_qs(binary_to_list(Binary));
parse_qs(String) ->
    parse_qs(String, []).

parse_qs([], Acc) ->
    lists:reverse(Acc);
parse_qs(String, Acc) ->
    {Key, Rest} = parse_qs_key(String),
    {Value, Rest1} = parse_qs_value(Rest),
    parse_qs(Rest1, [{Key, Value} | Acc]).

parse_qs_key(String) ->
    parse_qs_key(String, []).

parse_qs_key([], Acc) ->
    {qs_revdecode(Acc), ""};
parse_qs_key([$= | Rest], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_key(Rest=[$; | _], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_key(Rest=[$& | _], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_key([C | Rest], Acc) ->
    parse_qs_key(Rest, [C | Acc]).

parse_qs_value(String) ->
    parse_qs_value(String, []).

parse_qs_value([], Acc) ->
    {qs_revdecode(Acc), ""};
parse_qs_value([$; | Rest], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_value([$& | Rest], Acc) ->
    {qs_revdecode(Acc), Rest};
parse_qs_value([C | Rest], Acc) ->
    parse_qs_value(Rest, [C | Acc]).

qs_revdecode(S) ->
    qs_revdecode(S, []).

qs_revdecode([], Acc) ->
    Acc;
qs_revdecode([$+ | Rest], Acc) ->
    qs_revdecode(Rest, [$\s | Acc]);
qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) ->
    qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]);
qs_revdecode([C | Rest], Acc) ->
    qs_revdecode(Rest, [C | Acc]).
%% }}}

-spec initialize({Module::atom(), Name::string()}) -> webtool_callback_result().
initialize({Module, Name}) ->
    ML = atom_to_list(Module),
    {Module, [
        {web_data, {"Bmula " ++ Name, "/" ++ ML ++ "/bmula_webtool/index?view=" ++ ML}},
        {alias, {erl_alias, "/" ++ ML, [?MODULE]}}
    ]}.

index(_, "view=" ++ ML) ->
    [<<"
<html>
    <head>
        <title>">>, ML, <<"</title>
    </head>
    <frameset cols=\"15%,85%\">
        <frame               src=\"/">>, ML, <<"/bmula_webtool/left?view=">>, ML, <<"\" />
        <frame name=\"main\" src=\"/">>, ML, <<"/bmula_webtool/right?view=">>, ML, <<"\" />
    </frameset>
</html>">>].

left(_, QueryString) ->
    side(left, QueryString).

right(_, QueryString) ->
    side(right, QueryString).

side(Side, QueryString) ->
    Qs0 = parse_qs(QueryString),
    {value, {_, ML}, Qs} = lists:keytake("view", 1, Qs0),
    Module = list_to_atom(ML),
    [<<"
<html>
    <head>
        <title>">>, ML, <<"</title>
    </head>
    <body>">>,
        Module:webtool(Side, Qs, fun(Props0, Text) ->
            Props = case lists:keysearch(view, 1, Props0) of
                false -> [{view, ML} | Props0];
                _     -> Props0
            end,
            ["<a target=\"main\" href=\"/", ML, "/bmula_webtool/right?", urlencode(Props), "\">", Text, "</a>"]
        end),
    <<"</body>
</html>">>].

to_str(A) ->
    list_to_binary(io_lib:format("~9999p", [A])).

dl(Props) ->
    dl(fun
        ({K, V}) -> [<<"<dt>">>, to_str(K), <<"</dt><dd>">>, to_str(V), <<"</dd>">>];
        (K)      -> [<<"<dt>">>, to_str(K), <<"</dt><dd>true</dd>">>]
    end, Props).

dl(F, Props) ->
    [<<"<dl>">>, lists:flatmap(fun(KV) ->
        try
            F(KV)
        catch
            error:function_clause -> []
        end
    end, Props), <<"</dl>">>].

ul(List) ->
    ul(fun(X) -> to_str(X) end, List).
ul(F, List) ->
    [<<"<ul>">>, lists:flatmap(fun(X) ->
        try
            [<<"<li>">>, F(X), <<"</li>">>]
        catch
            error:function_clause -> []
        end
    end, List), <<"</ul>">>].
